Fix JSDoc @type completion inside function argument#62935
Fix JSDoc @type completion inside function argument#62935caverac wants to merge 10 commits intomicrosoft:mainfrom
Conversation
…o reflect reported issue
|
@microsoft-github-policy-service agree |
There was a problem hiding this comment.
Pull request overview
This PR fixes JSDoc @type completion that was not working in two specific scenarios: inline JSDoc comments on function parameters with named arguments, and inline JSDoc comments without parameter names (orphaned JSDoc). The fix enables proper type completions and namespace member completions in these contexts.
Key Changes
- Added handling for inline JSDoc on function parameters where
isInCommentreturns undefined but a JSDocTag can be found via the current token - Added fallback logic to parse and handle orphaned JSDoc comments (not attached to AST nodes) by using
parseIsolatedJSDocComment - Extended the completion blocker check to skip blocking when inside JSDoc type expressions
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
tests/cases/fourslash/jsdocTypeCompletionInFunctionParameter.ts |
Comprehensive test cases covering inline JSDoc on function parameters with and without names, testing both @import and @typedef scenarios |
tests/cases/fourslash/jsdocTypeCompletionOrphanedBranches.ts |
Extensive branch coverage tests for orphaned JSDoc handling, including edge cases like nested qualified names, empty JSDoc, and non-JSDoc comments |
src/services/completions.ts |
Core implementation changes adding orphaned JSDoc parsing, inline JSDoc handling, and a helper function to find namespace identifiers from JSDoc import tags |
src/services/completions.ts
Outdated
| function findJsDocImportNamespaceIdentifier(sf: SourceFile, namespaceName: string): Identifier | undefined { | ||
| for (const statement of sf.statements) { | ||
| const jsDocNodes = (statement as Node & { jsDoc?: JSDoc[] }).jsDoc; | ||
| for (const jsDoc of jsDocNodes || []) { |
There was a problem hiding this comment.
The null coalescing operator should be used here to avoid creating an empty array when jsDocNodes is undefined. This avoids unnecessary iteration and is more idiomatic. Consider changing jsDocNodes || [] to jsDocNodes ?? [] to only substitute when the value is nullish rather than any falsy value.
| for (const jsDoc of jsDocNodes || []) { | |
| for (const jsDoc of jsDocNodes ?? []) { |
src/services/completions.ts
Outdated
| for (const statement of sf.statements) { | ||
| const jsDocNodes = (statement as Node & { jsDoc?: JSDoc[] }).jsDoc; | ||
| for (const jsDoc of jsDocNodes || []) { | ||
| for (const tag of jsDoc.tags || []) { |
There was a problem hiding this comment.
The null coalescing operator should be used here to avoid creating an empty array when jsDoc.tags is undefined. This avoids unnecessary iteration and is more idiomatic. Consider changing jsDoc.tags || [] to jsDoc.tags ?? [] to only substitute when the value is nullish rather than any falsy value.
| for (const tag of jsDoc.tags || []) { | |
| for (const tag of jsDoc.tags ?? []) { |
src/services/completions.ts
Outdated
| node = orphanedJsDocQualifiedNameLeft; | ||
| } | ||
|
|
||
|
|
There was a problem hiding this comment.
There's an extra blank line here that should be removed to maintain consistent code formatting throughout the file.
src/services/completions.ts
Outdated
| if (typeExpression.kind === SyntaxKind.JSDocTypeExpression) { | ||
| const typeNode = typeExpression.type; | ||
| if (isTypeReferenceNode(typeNode) && isQualifiedName(typeNode.typeName) && posInComment > typeNode.typeName.left.end && isIdentifier(typeNode.typeName.left)) { | ||
| const leftText = commentText.substring(typeNode.typeName.left.pos, typeNode.typeName.left.end).trim(); |
There was a problem hiding this comment.
The .trim() call here may cause issues if the identifier text contains leading or trailing whitespace in the parsed AST positions. Since the positions from the parsed JSDoc should already point to the exact identifier text without whitespace, the trim call is unnecessary and could potentially cause the namespace lookup to fail if the positions include any surrounding whitespace. Consider removing the .trim() call to use the exact substring as indicated by the parsed positions.
| const leftText = commentText.substring(typeNode.typeName.left.pos, typeNode.typeName.left.end).trim(); | |
| const leftText = commentText.substring(typeNode.typeName.left.pos, typeNode.typeName.left.end); |
Fixes #62281
Problem
JSDoc
@typecompletion does not work for inline JSDoc comments on function parameters. There are two different problems at play in this issue:Inline JSDoc on function parameter
Inline JSDoc comment with no parameter name
In neither of those cases there are completion suggestions at the
/*cursor*/location.Cause
In file
src/services/completions.tsInline JSDoc on function parameter
In this case the check
if (insideComment)evaluates tofalsebecauseinsideCommentisundefined. This, despite the fact thatgetTokenAtPositionreturns a valid token inside the JSDoc tree.More specifically:
currentToken = getTokenAtPosition(...)CloseBraceToken"}"insideComment = isInComment(...)undefinedif (insideComment)Since
insideCommentis falsy, theif (insideComment)block is skipped entirely andinsideJsDocTagTypeExpressionis never set totrue, so no JSDoc type completions are provided.Inline JSDoc comment with no parameter name
Even after the issue above is fixed, there's a second problem:
currentToken = getTokenAtPosition(...)CloseParenToken ")"insideComment = isInComment(...){kind:3, pos:50, end:67}(truthy)if (insideComment)if (hasDocComment(sourceFile, position))false- JSDoc is orphaned, not attached to ASTtag = getJsDocTagAtPosition(currentToken, position)undefined- can't find tag from)tokenif (!insideJsDocTagTypeExpression && !insideJsDocImportTag)return undefinedBoth of these cases fail to generate completion, however, they fail for two fundamentally different reasons:
isInCommenthasDocCommentundefinedisInCommentis falsy, skips entireifblocktruthyfalseifblock but JSDoc not attached to ASTSolution
Inline JSDoc on function parameter
For this case we just need to add an
elseclause to handle the case whereinsideCommentis falsy, but the current token is inside a JSDoc comment.Inline JSDoc comment with no parameter name
This one is trickier. Since
hasDocCommentis false, we can't use the normal JSDoc APIs, instead we manually extract the comment text and parse it. This introduces a potential performance hit, but tests indicate it is not too severe.Testing performance
I ran tests for four different scenarios:
orphaned-qualified:function foo(/** @type {t.} */) {}- WORST CASE (triggers full orphaned handling + namespace search)orphaned-simple:function foo(/** @type {str} */) {}- orphaned JSDoc, but no namespace search needednamed-param:function foo(/** @type {t.} */ param) {}- used addedelseclause to handle JSDoc with named paramno-jsdoc:function foo(param) {}- baseline, no JSDoc processingSome things to highlight
mainis faster, but that is because code gives up early and does not completeno-jsdocbaseline is added for reference, to show overhead of the processing in general (dashed gray line)Test Plan